/**
 * Copyright (C) 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package safesax;

import org.xml.sax.Locator;
import org.xml.sax.SAXParseException;

import java.util.ArrayList;

/**
 * An XML element. Provides access to child elements and hooks to listen for
 * events related to this element.
 *
 * @see RootElement
 */
public class Element {

  final String uri;
  final String localName;
  final int depth;
  final Element parent;
  Children children;

  ArrayList<Element> requiredChilden;
  boolean visited;
  
  StartElementListener startElementListener;
  EndElementListener endElementListener;
  EndTextElementListener endTextElementListener;

  Element(Element parent, String uri, String localName, int depth) {
    this.parent = parent;
    this.uri = uri;
    this.localName = localName;
    this.depth = depth;
  }

  /**
   * Gets the child element with the given name. Uses an empty string as the
   * namespace.
   */
  public Element getChild(String localName) {
    return getChild("", localName);
  }

  /**
   * Gets the child element with the given name.
   */
  public Element getChild(String uri, String localName) {
    if (endTextElementListener != null) {
      throw new IllegalStateException("This element already has an end"
          + " text element listener. It cannot have children.");
    }

    if (children == null) {
      children = new Children();
    }

    return children.getOrCreate(this, uri, localName);
  }

  /**
   * Gets the child element with the given name. Uses an empty string as the
   * namespace. We will throw a {@link org.xml.sax.SAXException} at parsing time
   * if the specified child is missing. This helps you ensure that your
   * listeners are called.
   */
  public Element requireChild(String localName) {
    return requireChild("", localName);
  }

  /**
   * Gets the child element with the given name. We will throw a {@link
   * org.xml.sax.SAXException} at parsing time if the specified child is
   * missing. This helps you ensure that your listeners are called.
   */
  public Element requireChild(String uri, String localName) {
    Element child = getChild(uri, localName);

    if (requiredChilden == null) {
      requiredChilden = new ArrayList<Element>();
      requiredChilden.add(child);
    } else {
      if (!requiredChilden.contains(child)) {
        requiredChilden.add(child);
      }
    }

    return child;
  }

  /**
   * Sets start and end element listeners at the same time.
   */
  public void setElementListener(ElementListener elementListener) {
    setStartElementListener(elementListener);
    setEndElementListener(elementListener);
  }

  /**
   * Sets start and end text element listeners at the same time.
   */
  public void setTextElementListener(TextElementListener elementListener) {
    setStartElementListener(elementListener);
    setEndTextElementListener(elementListener);
  }

  /**
   * Sets a listener for the start of this element.
   */
  public void setStartElementListener(
      StartElementListener startElementListener) {
    if (this.startElementListener != null) {
      throw new IllegalStateException(
          "Start element listener has already been set.");
    }
    this.startElementListener = startElementListener;
  }

  /**
   * Sets a listener for the end of this element.
   */
  public void setEndElementListener(EndElementListener endElementListener) {
    if (this.endElementListener != null) {
      throw new IllegalStateException(
          "End element listener has already been set.");
    }
    this.endElementListener = endElementListener;
  }

  /**
   * Sets a listener for the end of this text element.
   */
  public void setEndTextElementListener(
      EndTextElementListener endTextElementListener) {
    if (this.endTextElementListener != null) {
      throw new IllegalStateException(
          "End text element listener has already been set.");
    }

    if (children != null) {
      throw new IllegalStateException("This element already has children."
          + " It cannot have an end text element listener.");
    }

    this.endTextElementListener = endTextElementListener;
  }

  @Override
  public String toString() {
    return toString(uri, localName);
  }

  static String toString(String uri, String localName) {
    return "'" + (uri.equals("") ? localName : uri + ":" + localName) + "'";
  }

  /**
   * Clears flags on required children.
   */
  void resetRequiredChildren() {
    ArrayList<Element> requiredChildren = this.requiredChilden;
    if (requiredChildren != null) {
      for (int i = requiredChildren.size() - 1; i >= 0; i--) {
        requiredChildren.get(i).visited = false;
      }
    }
  }

  /**
   * Throws an exception if a required child was not present.
   */
  void checkRequiredChildren(Locator locator) throws SAXParseException {
    ArrayList<Element> requiredChildren = this.requiredChilden;
    if (requiredChildren != null) {
      for (int i = requiredChildren.size() - 1; i >= 0; i--) {
        Element child = requiredChildren.get(i);
        if (!child.visited) {
          throw new BadXmlException(
              "Element named " + this + " is missing required"
                  + " child element named "
                  + child + ".", locator);
        }
      }
    }
  }
}
